package se.webbzon.oschi01.hash;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.nio.charset.Charset;

public class MD5 {
	
	// A table containing the constants  
	// k[i] = Math.floor(Math.abs(sin(i + 1)) * Math.pow(2,32));
	private static final int[] k = {
		0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
		0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
		0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
		0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
		0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
		0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
		0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
		0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
		0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
		0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
		0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
		0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
		0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
		0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
		0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
		0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391};
	
	// A table containing the rotation distance for each round
	private static final int[] s = {
		7, 12, 17, 22, 
		5,  9, 14, 20, 
		4, 11, 16, 23, 
		6, 10, 15, 21};

	// The initial content of a md5 instance
	private static int 	initA = 0x67452301, 
						initB = (int) 0xEFCDAB89L, 
						initC = (int) 0x98BADCFEL, 
						initD = 0x10325476;
	
	// The content of this md5 instance
	private int a, b, c, d;
	
	// A 512-bits buffer used to store the current chunk
	private final ByteBuffer buff;
	
	private final IntBuffer intBuff;
	
	// The total length of the digested data
	private int length;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new MD5-instance. **/
	public MD5() {
		a = initA;
		b = initB;
		c = initC;
		d = initD;
		buff = ByteBuffer.allocate(64);
		buff.order(ByteOrder.LITTLE_ENDIAN);
		intBuff = buff.asIntBuffer();
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Returns the MD5-sum calculated by this instance. **/
	public byte[] sum() {
		int newLength = length + 1;
		while (newLength % 64 != 56)
			newLength++;
		byte[] tail = new byte[newLength - length + 8];
		tail[0] = (byte) 0x80;
		putInt(length*8,tail,tail.length-8);
		putInt(length>>29,tail,tail.length-4);
		addBytes(tail,0,tail.length);
		
		ByteBuffer buff = ByteBuffer.allocate(16);
		buff.order(ByteOrder.LITTLE_ENDIAN);
		buff.putInt(a);
		buff.putInt(b);
		buff.putInt(c);
		buff.putInt(d);
		return buff.array();
	}
	
	/** Adds a single byte to this MD5-instance. **/
	public void addByte(byte b) {
		this.length ++;
		if (1 >= buff.remaining()) {
			final int[] array = new int[16];
			buff.put(b);
			intBuff.get(array);
			buff.rewind();
			intBuff.rewind();
			sumChunk(array,0);
		} else
			buff.put(b);
	}
	
	/** Adds a number of bytes to this MD5-instance. **/
	public void addBytes(byte[] data, int offset, int length) {
		this.length += length;
		if (length >= buff.remaining()) {
			final int[] array = new int[16];
			while (length >= buff.remaining()) {
				int digest = buff.remaining();
				buff.put(data, offset, digest);
				intBuff.get(array);
				buff.rewind();
				intBuff.rewind();
				sumChunk(array,0);
				offset += digest;
				length -= digest;
			}
		}
		if (length > 0)
			buff.put(data, offset, length);
	}
	
	/** Adds a string to this MD5-instance. **/
	public void addString(String str, Charset charset) {
		byte[] data = str.getBytes(charset);
		addBytes(data,0,data.length);
	}
	
	/** Adds an integer to this MD5-instance by using little endian byte
	 * order. **/
	public void addInt(int i) {
		byte[] data = new byte[] { 	(byte) i, 			(byte) (i >> 8), 
									(byte) (i >> 16), 	(byte) (i >> 24)};
		addBytes(data,0,data.length);
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Adds the md5 sum of the provided 512-bit chunk. **/
	private final void sumChunk(int[] data, int offset) {
		int xa = a, xb = b, xc = c, xd = d;
		for (int i = 0; i < 64; i++) {
			final int indexDiv16 = i >>> 4;
			final int f;
			final int index;
			if (i < 16) {
				f = (b & c) | (~b & d);
				index = i;
			} else if (i < 32) {
				f = (b & d) | (c & ~d);
				index = (5 * i + 1) & 0x0F;
			} else if (i < 48) {
				f = b ^ c ^ d;
				index = (3 * i + 5) & 0x0F;
			} else {
				f = c ^ (b | ~d);
				index = (7 * i) & 0x0F;
			}
			int t = d;
			d = c;
			c = b;
			b = b + Integer.rotateLeft(a + f + k[i] + data[offset + index], s[(indexDiv16 << 2) | (i & 3)]);
			a = t;
		}
		a += xa; b += xb; c += xc; d += xd;
	}
	
	/** Puts an integer in the provided byte array at the given offset. **/
	private static void putInt(int x, byte[] array, int offset) {
		array[offset] = (byte) x;
		array[offset+1] = (byte) (x >> 8);
		array[offset+2] = (byte) (x >> 16);
		array[offset+3] = (byte) (x >> 24);
	}	

}
